Skip to content

feat(ai-observability): add time-bucketed breakdown to personal spend endpoint#69891

Merged
webjunkie merged 16 commits into
masterfrom
posthog-code/personal-spend-hourly-breakdown
Jul 15, 2026
Merged

feat(ai-observability): add time-bucketed breakdown to personal spend endpoint#69891
webjunkie merged 16 commits into
masterfrom
posthog-code/personal-spend-hourly-breakdown

Conversation

@webjunkie

@webjunkie webjunkie commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Problem

The personal spend endpoint (GET /api/llm_analytics/@me/spend/) only exposes a daily total-cost series (by_day), so clients like the PostHog Code desktop app can't render a fine-grained usage view. In particular, prompt-cache cold starts — where a stale session is revived and its whole context is re-written to the cache at the cache-write rate instead of being read back cheaply — are invisible: they need sub-hour resolution and a per-component cost split to stand out. Since the prompt-cache TTL is 5 minutes, a 5-minute bucket is roughly one turn, which isolates a revival spike that hourly buckets would dilute.

Why: we want the PostHog Code app to show a 24h usage chart granular enough that a sudden burst of full-price tokens (a revived cold session) is visually obvious. A companion PR in the PostHog Code repo (PostHog/code#3329) consumes this.

Changes

  • New opt-in bucket_minutes query param (5, 15, 30, or 60) on /api/llm_analytics/@me/spend/.
  • When set, the response additionally carries by_bucket: a time-ascending UTC series (grouped via toStartOfInterval) with per-bucket cost_usd plus four disjoint cost components (input_cost_usd, output_cost_usd, cache_read_cost_usd, cache_creation_cost_usd) and the matching token sums. The stored $ai_input_cost_usd includes cache read/write costs, so uncached input is derived per event as input minus cache read/write (clamped at zero); the components sum to cost_usd when the breakdown is present, so they stack without double counting cache spend. The bucket size is echoed on the breakdown.
  • The window may span at most 600 buckets of the chosen size (400 otherwise) — 48h at 5-minute buckets, 8+ days hourly; by_bucket ignores limit like by_day does.
  • Cache key, EU redirect param allowlist, and the EU→US proxy all carry the new param.

Note: CI codegen keeps the generated OpenAPI types current (no DB available for hogli build:openapi in the authoring environment).

How did you test this code?

Added tests to test_personal_spend.py, each catching a distinct regression:

  • test_by_bucket_groups_cost_components_per_utc_hour — wrong column order/mapping or grouping in the new HogQL query, and product-scope leakage.
  • test_by_bucket_five_minute_buckets_split_within_the_hour — the bucket size not actually being applied (two calls 15 minutes apart must split at 5-minute resolution).
  • test_by_bucket_defaults_components_to_zero_when_breakdown_missing — fallback-priced events (total cost only) must produce zero components, not errors.
  • test_by_bucket_absent_unless_requestedby_bucket leaking into default responses.
  • test_cache_key_includes_bucket_minutes — a cached bucketless payload being served to a bucketed request (would silently drop by_bucket).
  • test_bucket_window_cap (parameterized) + test_unsupported_bucket_size_rejected — the 600-bucket cap or the size allowlist being dropped.

Could not run ClickHouse-backed tests in this sandbox (no database); verified locally that both files compile, the new HogQL parses via parse_select (toStartOfInterval/toIntervalMinute are whitelisted), and the response serializer omits by_bucket when absent. CI runs the full suite.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

Endpoint is documented via its OpenAPI schema (@extend_schema description and serializer help_text updated in this PR).

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Authored with PostHog Code (Claude). Skills invoked: /improving-drf-endpoints, /writing-tests.
  • Investigated where PostHog Code usage/cost data originates: the LLM gateway captures one $ai_generation event per LLM call with LiteLLM's per-side cost breakdown, and this endpoint is the sanctioned user-scoped surface over those events — so the view was added here rather than as client-side HogQL (the events live in an internal analytics team, not the user's project).
  • Started as an hourly=true boolean; generalized to bucket_minutes after deciding hourly resolution dilutes single-turn revival spikes (cache TTL is 5 minutes, so 5-minute buckets ≈ one turn).
  • Chose an opt-in param + optional response field over a new endpoint to reuse the existing auth/throttle/cache/EU-proxy machinery unchanged.

Created with PostHog Code

Opt-in `hourly=true` query param adds a `by_hour` breakdown to
GET /api/llm_analytics/@me/spend/: an hour-ascending UTC series with
per-hour cost split into uncached input / output / cache read / cache
creation components (plus matching token sums), capped to windows of
8 days or less. Enables clients to render a 24h view where prompt-cache
cold starts stand out as cache-creation-dominated bars.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@github-actions

Copy link
Copy Markdown
Contributor

Hey @webjunkie! 👋

It looks like your git author email on this PR isn't your @posthog.com address (julian.bez@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — 🟢 -5.30 MiB (-7.6%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.76 MiB · 🟢 -5.30 MiB (-7.6%)

File Size Δ vs base
render-query/src/render-query/render-query.js 21.25 MiB 🟢 -2.91 MiB (-12.0%)
toolbar/src/toolbar/toolbar-app.js 70.9 KiB 🟢 -1.65 MiB (-96.0%)
toolbar/src/scenes/product-tours/editor/chunk-generateStepHtml.js 647.0 KiB 🔺 +647.0 KiB (new)
exporter/src/exporter/scenes/ExporterNotebookScene.js 3.20 MiB 🔺 +333.0 KiB (+11.3%)
exporter/src/exporter/scenes/ExporterDashboardScene.js 272.5 KiB 🔺 +265.0 KiB (+3562.7%)
toolbar/src/toolbar/debug/chunk-EventDebugMenu.js 255.5 KiB 🔺 +255.5 KiB (new)
exporter/_parent/products/workflows/frontend/Workflows/WorkflowScene.js removed 🟢 -157.4 KiB (-100.0%)
exporter/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js removed 🟢 -149.5 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/AIObservabilityTraceScene.js removed 🟢 -130.1 KiB (-100.0%)
exporter/_parent/products/customer_analytics/frontend/CustomerAnalyticsScene.js removed 🟢 -119.7 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/AIObservabilityScene.js removed 🟢 -115.3 KiB (-100.0%)
exporter/_parent/products/error_tracking/frontend/scenes/ErrorTrackingIssueScene/ErrorTrackingIssueScene.js removed 🟢 -100.7 KiB (-100.0%)
exporter/_parent/products/tracing/frontend/TracingScene.js removed 🟢 -88.9 KiB (-100.0%)
toolbar/src/toolbar/surveys/chunk-SurveySidebar.js 86.4 KiB 🔺 +86.4 KiB (new)
exporter/_parent/products/replay_vision/frontend/replay_scanners/ReplayScanner.js removed 🟢 -70.2 KiB (-100.0%)
exporter/_parent/products/workflows/frontend/WorkflowsScene.js removed 🟢 -60.7 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/evaluations/AIObservabilityEvaluation.js removed 🟢 -59.8 KiB (-100.0%)
exporter/_parent/products/legal_documents/frontend/scenes/LegalDocumentNewScene.js removed 🟢 -59.1 KiB (-100.0%)
exporter/_parent/products/endpoints/frontend/EndpointScene.js removed 🟢 -56.5 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/clusters/AIObservabilityClustersScene.js removed 🟢 -53.1 KiB (-100.0%)
exporter/_parent/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsScene.js removed 🟢 -53.0 KiB (-100.0%)
exporter/_parent/products/conversations/frontend/scenes/ticket/SupportTicketScene.js removed 🟢 -48.8 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewRunScene.js removed 🟢 -47.0 KiB (-100.0%)
posthog-app/_parent/products/review_hog/frontend/CodeReviewScene.js 45.3 KiB 🔺 +45.3 KiB (new)
exporter/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js removed 🟢 -43.3 KiB (-100.0%)
exporter/_parent/products/error_tracking/frontend/scenes/ErrorTrackingScene/ErrorTrackingScene.js removed 🟢 -42.7 KiB (-100.0%)
toolbar/src/toolbar/stats/chunk-HeatmapToolbarMenu.js 37.7 KiB 🔺 +37.7 KiB (new)
exporter/_parent/products/ai_observability/frontend/prompts/LLMPromptScene.js removed 🟢 -37.5 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/playground/AIObservabilityPlaygroundScene.js removed 🟢 -37.1 KiB (-100.0%)
exporter/_parent/products/growth/frontend/IdentityMatchingScene.js removed 🟢 -35.5 KiB (-100.0%)
exporter/_parent/products/metrics/frontend/MetricsScene.js removed 🟢 -34.4 KiB (-100.0%)
exporter/_parent/products/data_warehouse/frontend/scenes/SchemaScene/SchemaScene.js removed 🟢 -34.0 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/evaluations/AIObservabilityEvaluationsScene.js removed 🟢 -32.7 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/tags/AIObservabilityTag.js removed 🟢 -32.0 KiB (-100.0%)
exporter/_parent/products/revenue_analytics/frontend/RevenueAnalyticsScene.js removed 🟢 -29.9 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/replay_scanners/ScannerEditorScene.js removed 🟢 -28.8 KiB (-100.0%)
exporter/_parent/products/actions/frontend/pages/Action.js removed 🟢 -28.7 KiB (-100.0%)
exporter/_parent/products/data_warehouse/DataWarehouseScene.js removed 🟢 -28.4 KiB (-100.0%)
exporter/_parent/products/endpoints/frontend/EndpointsScene.js removed 🟢 -27.5 KiB (-100.0%)
exporter/_parent/products/links/frontend/LinkScene.js removed 🟢 -25.2 KiB (-100.0%)
exporter/_parent/products/business_knowledge/frontend/scenes/BusinessKnowledgeScene.js removed 🟢 -24.6 KiB (-100.0%)
posthog-app/src/scenes/inbox/InboxScene.js 659.0 KiB 🔺 +23.6 KiB (+3.7%)
exporter/_parent/products/session_summaries/frontend/SessionGroupSummaryScene.js removed 🟢 -23.6 KiB (-100.0%)
exporter/_parent/products/logs/frontend/LogsScene.js removed 🟢 -23.2 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/replay_scanners/ReplayScannersScene.js removed 🟢 -22.7 KiB (-100.0%)
exporter/src/scenes/data-pipelines/event-filtering/EventFilterScene.js removed 🟢 -22.6 KiB (-100.0%)
posthog-app/src/scenes/feature-flags/FeatureFlagForm.js 22.5 KiB 🔺 +22.5 KiB (new)
exporter/_parent/products/engineering_analytics/frontend/scenes/PullRequestDetailScene.js removed 🟢 -21.8 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/clusters/AIObservabilityClusterScene.js removed 🟢 -21.7 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/observations/ReplayObservation.js removed 🟢 -21.5 KiB (-100.0%)
posthog-app/src/scenes/feature-flags/FeatureFlag.js 94.7 KiB 🟢 -20.9 KiB (-18.0%)
exporter/_parent/products/ai_observability/frontend/datasets/AIObservabilityDatasetScene.js removed 🟢 -20.6 KiB (-100.0%)
exporter/src/scenes/models/ModelsScene.js removed 🟢 -20.0 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewSnapshotOverviewScene.js removed 🟢 -19.7 KiB (-100.0%)
exporter/_parent/products/live_debugger/frontend/LiveDebugger.js removed 🟢 -19.5 KiB (-100.0%)
exporter/_parent/products/subscriptions/frontend/scenes/SubscriptionScene.js removed 🟢 -19.2 KiB (-100.0%)
exporter/_parent/products/logs/frontend/scenes/LogsAlertDetailScene/LogsAlertDetailScene.js removed 🟢 -18.5 KiB (-100.0%)
exporter/_parent/products/engineering_analytics/frontend/scenes/WorkflowRunsScene.js removed 🟢 -17.8 KiB (-100.0%)
exporter/_parent/products/workflows/frontend/TemplateLibrary/MessageTemplate.js removed 🟢 -17.2 KiB (-100.0%)
toolbar/src/toolbar/actions/chunk-ActionsToolbarMenu.js 16.8 KiB 🔺 +16.8 KiB (new)
exporter/_parent/products/mcp_analytics/frontend/MCPAnalyticsToolDetail.js removed 🟢 -16.3 KiB (-100.0%)
exporter/_parent/products/managed_migrations/frontend/ManagedMigration.js removed 🟢 -15.8 KiB (-100.0%)
exporter/src/scenes/models/NodeDetailScene.js removed 🟢 -14.9 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/replay_scanners/ActionEditorScene.js removed 🟢 -14.8 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewSnapshotHistoryScene.js removed 🟢 -14.3 KiB (-100.0%)
toolbar/src/toolbar/product-tours/chunk-ProductToursSidebar.js 14.1 KiB 🔺 +14.1 KiB (new)
exporter/_parent/products/ai_gateway/frontend/AIGatewayScene.js removed 🟢 -13.3 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/tags/AIObservabilityTagsScene.js removed 🟢 -12.2 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewSettingsScene.js removed 🟢 -11.7 KiB (-100.0%)
toolbar/src/toolbar/experiments/chunk-ExperimentsToolbarMenu.js 11.2 KiB 🔺 +11.2 KiB (new)
exporter/_parent/products/tracing/frontend/TracingOperationScene.js removed 🟢 -10.9 KiB (-100.0%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ReplayScanner.js 82.8 KiB 🔺 +10.9 KiB (+15.1%)
exporter/_parent/products/user_interviews/frontend/UserInterview.js removed 🟢 -10.6 KiB (-100.0%)
posthog-app/_parent/products/tracing/frontend/TracingScene.js 100.0 KiB 🔺 +10.2 KiB (+11.3%)
exporter/_parent/products/feature_flags/frontend/staff/FeatureFlagsStaffToolsScene.js removed 🟢 -9.8 KiB (-100.0%)
exporter/_parent/products/customer_analytics/frontend/scenes/CustomerJourneyTemplatesScene/CustomerJourneyTemplatesScene.js removed 🟢 -9.6 KiB (-100.0%)
exporter/_parent/products/logs/frontend/scenes/LogsAlertNotificationDetailScene/LogsAlertNotificationDetailScene.js removed 🟢 -9.2 KiB (-100.0%)
exporter/_parent/products/tasks/frontend/SlackTaskContextScene.js removed 🟢 -9.2 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewRunsScene.js removed 🟢 -8.4 KiB (-100.0%)
exporter/src/scenes/data-pipelines/TransformationsScene.js removed 🟢 -8.4 KiB (-100.0%)
exporter/_parent/products/user_interviews/frontend/UserInterviewResponse.js removed 🟢 -8.3 KiB (-100.0%)
exporter/_parent/products/error_tracking/frontend/scenes/ErrorTrackingFingerprintsScene/ErrorTrackingIssueFingerprintsScene.js removed 🟢 -8.1 KiB (-100.0%)
exporter/_parent/products/engineering_analytics/frontend/scenes/WorkflowRunDetailScene.js removed 🟢 -8.0 KiB (-100.0%)
exporter/_parent/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsAuthorScene.js removed 🟢 -8.0 KiB (-100.0%)
exporter/_parent/products/data_warehouse/frontend/scenes/SourcesScene/SourcesScene.js removed 🟢 -8.0 KiB (-100.0%)
exporter/_parent/products/data_warehouse/frontend/scenes/SourceConnectScene/SourceConnectScene.js removed 🟢 -7.5 KiB (-100.0%)
exporter/_parent/products/customer_analytics/frontend/scenes/CustomerAnalyticsConfigurationScene/CustomerAnalyticsConfigurationScene.js removed 🟢 -7.2 KiB (-100.0%)
exporter/_parent/products/customer_analytics/frontend/scenes/CustomerJourneyBuilderScene/CustomerJourneyBuilderScene.js removed 🟢 -7.0 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/replay_scanners/VisionActionRunScene.js removed 🟢 -6.9 KiB (-100.0%)
exporter/_parent/products/subscriptions/frontend/scenes/SubscriptionsScene.js removed 🟢 -6.8 KiB (-100.0%)
exporter/_parent/products/feature_flags/frontend/FeatureFlagTemplatesScene.js removed 🟢 -6.8 KiB (-100.0%)
exporter/_parent/products/user_interviews/frontend/UserInterviews.js removed 🟢 -6.7 KiB (-100.0%)
exporter/_parent/products/legal_documents/frontend/scenes/LegalDocumentsScene.js removed 🟢 -6.6 KiB (-100.0%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ActionEditorScene.js 22.2 KiB 🔺 +6.4 KiB (+41.0%)
exporter/_parent/products/conversations/frontend/scenes/settings/SupportSettingsScene.js removed 🟢 -6.4 KiB (-100.0%)
exporter/_parent/products/logs/frontend/scenes/LogsSamplingDetailScene/LogsSamplingDetailScene.js removed 🟢 -6.4 KiB (-100.0%)
exporter/_parent/products/actions/frontend/pages/Actions.js removed 🟢 -6.3 KiB (-100.0%)
exporter/_parent/products/early_access_features/frontend/EarlyAccessFeature.js removed 🟢 -6.2 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/prompts/LLMPromptsScene.js removed 🟢 -6.1 KiB (-100.0%)
exporter/_parent/products/replay_vision/frontend/replay_scanners/VisionActionScene.js removed 🟢 -6.0 KiB (-100.0%)
posthog-app/src/scenes/insights/InsightScene.js 37.8 KiB 🟢 -5.8 KiB (-13.3%)
posthog-app/_parent/products/engineering_analytics/frontend/scenes/EngineeringAnalyticsScene.js 59.6 KiB 🔺 +5.8 KiB (+10.7%)
exporter/_parent/products/session_summaries/frontend/SessionGroupSummariesTable.js removed 🟢 -5.7 KiB (-100.0%)
toolbar/src/toolbar/flags/chunk-FlagsToolbarMenu.js 5.6 KiB 🔺 +5.6 KiB (new)
exporter/_parent/products/games/FlappyHog/FlappyHog.js removed 🟢 -5.6 KiB (-100.0%)
exporter/_parent/products/posthog_ai/frontend/components/tool/builtinToolRenderers.js removed 🟢 -5.6 KiB (-100.0%)
exporter/_parent/products/links/frontend/LinksScene.js removed 🟢 -5.5 KiB (-100.0%)
exporter/src/queries/schema.js 1.01 MiB 🔺 +5.2 KiB (+0.5%)
posthog-app/src/queries/schema.js 1.01 MiB 🔺 +5.2 KiB (+0.5%)
posthog-app/src/scenes/bootApp.js 5.2 KiB 🔺 +5.2 KiB (new)
exporter/_parent/products/games/368Hedgehogs/368Hedgehogs.js removed 🟢 -5.2 KiB (-100.0%)
posthog-app/_parent/products/feature_flags/frontend/staff/FeatureFlagsStaffToolsScene.js 15.5 KiB 🔺 +4.9 KiB (+46.6%)
exporter/_parent/products/ai_observability/frontend/LLMASessionFeedbackDisplay.js removed 🟢 -4.7 KiB (-100.0%)
toolbar/src/toolbar/web-vitals/chunk-WebVitalsToolbarMenu.js 4.4 KiB 🔺 +4.4 KiB (new)
exporter/_parent/products/ai_observability/frontend/datasets/AIObservabilityDatasetsScene.js removed 🟢 -4.4 KiB (-100.0%)
posthog-app/_parent/products/replay_vision/frontend/observations/ReplayObservation.js 19.6 KiB 🟢 -4.2 KiB (-17.8%)
exporter/_parent/products/early_access_features/frontend/EarlyAccessFeatures.js removed 🟢 -4.2 KiB (-100.0%)
exporter/_parent/products/posthog_ai/frontend/components/tool/posthogCodeToolRenderers.js removed 🟢 -4.2 KiB (-100.0%)
posthog-app/src/scenes/AuthenticatedShell.js 222.4 KiB 🔺 +4.1 KiB (+1.9%)
exporter/_parent/products/ai_observability/frontend/AIObservabilityUsers.js removed 🟢 -4.0 KiB (-100.0%)
exporter/src/exporter/scenes/ExporterRecordingScene.js 2.5 KiB 🟢 -3.8 KiB (-60.4%)
posthog-app/_parent/products/alerts/frontend/views/EditAlertModal.js 3.8 KiB 🔺 +3.8 KiB (new)
posthog-app/_parent/products/metrics/frontend/MetricsScene.js 39.1 KiB 🔺 +3.7 KiB (+10.4%)
exporter/_parent/products/customer_analytics/frontend/components/AccountRelationshipsInput/CyclotronJobInputAccountRelationships.js removed 🟢 -3.7 KiB (-100.0%)
exporter/_parent/products/logs/frontend/scenes/LogsSamplingNewScene/LogsSamplingNewScene.js removed 🟢 -3.5 KiB (-100.0%)
posthog-app/src/scenes/instance/QueryPerformance/QueryPerformance.js 32.5 KiB 🔺 +3.5 KiB (+11.9%)
exporter/_parent/products/data_warehouse/frontend/scenes/NewSourceScene/NewSourceScene.js removed 🟢 -3.4 KiB (-100.0%)
exporter/_parent/products/posthog_ai/frontend/components/tool/EditDiffRenderer.js removed 🟢 -3.4 KiB (-100.0%)
exporter/_parent/products/customer_analytics/frontend/components/AccountPropertiesInput/CyclotronJobInputAccountProperties.js removed 🟢 -3.3 KiB (-100.0%)
exporter/_parent/products/visual_review/frontend/scenes/VisualReviewIndexScene.js removed 🟢 -3.3 KiB (-100.0%)
posthog-app/src/scenes/authentication/email-mfa-verify/EmailMFAVerify.js removed 🟢 -3.2 KiB (-100.0%)
exporter/_parent/products/ai_observability/frontend/AIObservabilitySessionScene.js removed 🟢 -3.2 KiB (-100.0%)
exporter/src/queries/nodes/WebVitals/WebVitals.js 8.8 KiB 🟢 -3.2 KiB (-26.7%)
exporter/src/queries/Query/Query.js 2.5 KiB 🟢 -3.2 KiB (-56.0%)
toolbar/src/toolbar/surveys/chunk-SurveysToolbarMenu.js 3.2 KiB 🔺 +3.2 KiB (new)
exporter/src/exporter/scenes/ExporterInsightScene.js 4.7 KiB 🟢 -3.1 KiB (-40.2%)
exporter/_parent/products/data_warehouse/frontend/scenes/SourceScene/SourceScene.js removed 🟢 -3.1 KiB (-100.0%)
posthog-app/src/scenes/App.js 22.5 KiB 🔺 +2.8 KiB (+14.5%)
exporter/_parent/products/conversations/frontend/components/SlaBusinessHours/CyclotronJobInputBusinessHours.js removed 🟢 -2.6 KiB (-100.0%)
toolbar/src/toolbar/field-notes/chunk-FieldNotesOverlay.js 2.6 KiB 🔺 +2.6 KiB (new)
toolbar/src/toolbar/field-notes/chunk-FieldNotesToolbarMenu.js 2.3 KiB 🔺 +2.3 KiB (new)
exporter/_parent/products/conversations/frontend/scenes/tickets/SupportTicketsScene.js removed 🟢 -2.1 KiB (-100.0%)
exporter/_parent/products/skills/frontend/LLMSkillsScene.js removed 🟢 -1.8 KiB (-100.0%)
exporter/_parent/products/skills/frontend/LLMSkillScene.js removed 🟢 -1.8 KiB (-100.0%)
exporter/src/scenes/insights/views/BoxPlot/BoxPlot.js 3.2 KiB 🟢 -1.8 KiB (-36.6%)
exporter/_parent/products/posthog_ai/frontend/components/QuestionRenderer.js removed 🟢 -1.7 KiB (-100.0%)
toolbar/src/toolbar/product-tours/chunk-ProductToursToolbarMenu.js 1.7 KiB 🔺 +1.7 KiB (new)
posthog-app/src/lib/components/ActivityLog/describers.js 131.3 KiB 🔺 +1.6 KiB (+1.2%)
posthog-app/_parent/products/engineering_analytics/frontend/scenes/PullRequestDetailScene.js 24.1 KiB 🔺 +1.5 KiB (+6.7%)
exporter/_parent/products/alerts/frontend/views/EditAlertModal.js 1.5 KiB 🔺 +1.5 KiB (new)
posthog-app/src/scenes/cohorts/Cohort.js 35.7 KiB 🔺 +1.4 KiB (+4.1%)
posthog-app/_parent/products/dashboards/frontend/widgets/experiments/ExperimentsListWidget.js 5.9 KiB 🔺 +1.4 KiB (+30.9%)
posthog-app/_parent/products/dashboards/frontend/widgets/experiments/ExperimentResultsWidget.js 7.5 KiB 🔺 +1.4 KiB (+22.2%)
exporter/_parent/products/conversations/frontend/components/Assignee/CyclotronJobInputAssignee.js removed 🟢 -1.3 KiB (-100.0%)
posthog-app/_parent/products/ai_observability/frontend/AIObservabilityTraceScene.js 132.3 KiB 🔺 +1.3 KiB (+1.0%)
posthog-app/_parent/products/workflows/frontend/Workflows/WorkflowScene.js 120.0 KiB 🔺 +1.2 KiB (+1.0%)
posthog-app/_parent/products/workflows/frontend/WorkflowsScene.js 63.3 KiB 🔺 +1.2 KiB (+1.9%)
posthog-app/_parent/products/engineering_analytics/frontend/scenes/WorkflowRunsScene.js 19.6 KiB 🔺 +1.0 KiB (+5.6%)
posthog-app/src/scenes/insights/views/BoxPlot/BoxPlot.js 4.7 KiB 🟢 -1.0 KiB (-17.9%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.22 MiB · 22 files 🔺 +13.6 KiB (+1.1%) ███░░░░░░░ 28.4% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.14 MiB · 2,977 files 🔺 +63.4 KiB (+0.8%) █████████░ 88.0% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
280.3 KiB ../node_modules/.pnpm/posthog-js@1.400.1/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
222.7 KiB ../node_modules/.pnpm/posthog-js@1.400.1/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.9 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
92.7 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🟢 -128.97 MiB (-9.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1307.78 MiB · 🟢 -128.97 MiB (-9.0%)

ℹ️ ClickHouse migration SQL — 1 migration(s)

ClickHouse migration SQL per cloud environment

  • unset
    • all
      DROP TABLE IF EXISTS property_values_mv
      CREATE MATERIALIZED VIEW IF NOT EXISTS property_values_mv
      TO property_values
      AS SELECT
          team_id,
          property_type,
          property_key,
          property_value,
          property_count,
          coalesce(_timestamp, now()) as last_seen
      FROM posthog_test.kafka_property_values
  • US, EU
    • aux
      DROP TABLE IF EXISTS property_values_mv
      CREATE MATERIALIZED VIEW IF NOT EXISTS property_values_mv
      TO property_values
      AS SELECT
          team_id,
          property_type,
          property_key,
          property_value,
          property_count,
          coalesce(_timestamp, now()) as last_seen
      FROM posthog_test.kafka_property_values
  • DEV
    • data
      DROP TABLE IF EXISTS property_values_mv
      CREATE MATERIALIZED VIEW IF NOT EXISTS property_values_mv
      TO property_values
      AS SELECT
          team_id,
          property_type,
          property_key,
          property_value,
          property_count,
          coalesce(_timestamp, now()) as last_seen
      FROM posthog_test.kafka_property_values

@trunk-io

trunk-io Bot commented Jul 10, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

…inutes

Replace the hourly=true boolean with a bucket_minutes param (5, 15, 30,
or 60) so clients can pick sub-hour resolution: 5-minute buckets match
the prompt-cache TTL, isolating a cold-revival spike that hourly buckets
would dilute. The by_hour breakdown becomes by_bucket (row field
bucket_start, bucket size echoed) grouped via toStartOfInterval, and the
window cap is now a flat 600 buckets of the chosen size.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@webjunkie webjunkie changed the title feat(ai-observability): add hourly breakdown to personal spend endpoint feat(ai-observability): add time-bucketed breakdown to personal spend endpoint Jul 10, 2026
tests-posthog Bot and others added 2 commits July 10, 2026 08:56
…utes

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@webjunkie
webjunkie marked this pull request as ready for review July 10, 2026 10:29
Copilot AI review requested due to automatic review settings July 10, 2026 10:29
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 232ebd3 · box box-4fe6a708211d · ready in 637s (push → usable) · build log · rebuilds on every push, torn down on close

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "chore(mcp): update personal-spend tool s..." | Re-trigger Greptile

Comment on lines +168 to +172
bucket_minutes = serializers.ChoiceField(
choices=BUCKET_MINUTES_CHOICES,
required=False,
default=None,
allow_null=True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nullable Query Param Rejects Null

This field is optional, but marking it nullable makes generated clients expose bucket_minutes: null as a valid value. The frontend URL builder serializes that as bucket_minutes=null on a GET request, while DRF only accepts a real null value, so typed callers can send an allowed client value and receive a 400 instead of the bucketless response.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted in edbb2d9: dropped allow_null so the schema no longer advertises null (default=None stays for the omitted case), and the EU proxy now omits None-valued params from the signed body so the internal receiver accepts it.

Comment on lines +797 to +807
from_dt, to_dt = _resolve_window(date_from, date_to)
if bucket_minutes is not None and (to_dt - from_dt).total_seconds() > bucket_minutes * 60 * MAX_TIME_BUCKETS:
max_hours = bucket_minutes * MAX_TIME_BUCKETS // 60
raise exceptions.ValidationError(
{
"bucket_minutes": (
f"A window this large would exceed {MAX_TIME_BUCKETS} buckets at {bucket_minutes}-minute "
f"resolution — narrow the window to {max_hours} hours or less, or pick a larger bucket size."
)
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Boundary Window Exceeds Cap

A window exactly equal to bucket_minutes * 600 passes this check, but partial buckets at both edges can still produce 601 bucket starts. _fetch_by_bucket then returns those 601 rows as untruncated, so clients can receive more buckets than the documented maximum.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted in edbb2d9: validation now counts the bucket starts the window touches (unaligned edges add partial buckets) instead of comparing durations, and the fetch/truncate limits match, so by_bucket can never exceed 600 rows. Added a boundary test where a 600-bucket-duration window with a half-hour offset (601 starts) is rejected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in bucket_minutes query param to the personal spend endpoint (GET /api/llm_analytics/@me/spend/) so clients can request a sub-day, UTC time-bucketed spend series (by_bucket) with per-component cost/token breakdowns, enabling higher-resolution usage visualizations (for example, spotting prompt-cache cold-start “revival” spikes).

Changes:

  • Add bucket_minutes request validation, cache-key partitioning, EU redirect/proxy forwarding, and a new HogQL aggregation for by_bucket in the personal spend API.
  • Add ClickHouse-backed tests covering bucket sizing, window caps, cost-component grouping/defaults, and cache-key behavior.
  • Regenerate OpenAPI-derived schemas/types and MCP tool wiring to include the new query param and response shape.

Reviewed changes

Copilot reviewed 4 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
services/mcp/tests/unit/snapshots/tool-schemas/llma-personal-spend.json Updates MCP tool JSON schema snapshot to include bucket_minutes.
services/mcp/src/tools/generated/ai_observability.ts Forwards bucket_minutes into the generated MCP tool request query.
services/mcp/src/generated/ai_observability/api.ts Updates generated zod query schema/docstring to include bucket_minutes.
services/mcp/src/api/generated.ts Updates generated TS API types to include by_bucket and the bucket_minutes param enum.
products/ai_observability/frontend/generated/api.ts Updates generated frontend API client docs/types to mention bucket_minutes / by_bucket.
products/ai_observability/frontend/generated/api.schemas.ts Updates generated frontend schema types for the new by_bucket breakdown.
products/ai_observability/backend/api/test/test_personal_spend.py Adds ClickHouse tests for by_bucket, window caps, and cache key behavior.
products/ai_observability/backend/api/personal_spend.py Implements request param validation, new bucketed HogQL query, response serialization, caching, and EU forwarding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 166 to +180
help_text="If true, bypass the result cache and re-run the underlying queries against ClickHouse.",
)
bucket_minutes = serializers.ChoiceField(
choices=BUCKET_MINUTES_CHOICES,
required=False,
default=None,
allow_null=True,
help_text=(
"When set, additionally return a `by_bucket` breakdown: a time-ascending UTC cost series for "
"the scoped product at this bucket size in minutes, with per-bucket cost split into uncached "
"input / output / cache read / cache creation components plus the matching token sums. "
f"Supported bucket sizes: {', '.join(str(c) for c in BUCKET_MINUTES_CHOICES)}. The window may "
f"span at most {MAX_TIME_BUCKETS} buckets of the chosen size (e.g. 48 hours at 5-minute "
"buckets)."
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in edbb2d9: docs and the constants comment now say 50 hours at 5-minute buckets / 25 days hourly. The regenerated OpenAPI/MCP artifacts follow via CI codegen.

Comment thread products/ai_observability/backend/api/personal_spend.py Outdated
chatgpt-codex-connector[bot]

This comment was marked as outdated.

The test used the default 30-day window, which at 60-minute buckets is
720 buckets — rejected by the endpoint's own 600-bucket cap with a 400.
Pin the window to a day so the test exercises the cache-key behavior it
targets instead of the window validation.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
Drop allow_null from bucket_minutes: a nullable query param makes
generated clients advertise null, which serializes to the literal
string "null" in a GET query and gets rejected. The EU proxy now omits
None-valued params from the signed cross-region body accordingly.

Cap validation now counts the bucket starts the window touches
(unaligned edges add partial buckets) instead of comparing durations,
so by_bucket can never return more than 600 rows; fetch and truncate
limits match. Also correct the cap arithmetic in docs (600 buckets is
50 hours at 5-minute resolution, not 48) and drop em dashes from the
added API text.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@github-actions
github-actions Bot requested a deployment to preview-pr-69891 July 10, 2026 11:39 In progress
Comment thread products/ai_observability/frontend/generated/api.schemas.ts
chatgpt-codex-connector[bot]

This comment was marked as outdated.

bucket_minutes = serializers.ChoiceField(
choices=BUCKET_MINUTES_CHOICES,
required=False,
default=None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's right

) -> str:
to_slot = date_to or "_now"
return f"personal_spend:{email}:{date_from}:{to_slot}:{product}:{limit}"
return f"personal_spend:{email}:{date_from}:{to_slot}:{product}:{limit}:{bucket_minutes or 0}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make it so we only append the :{bucket_minutes} part if the argument is present, so existing cache keys remain valid?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in af97495: the :{bucket_minutes} slot is now appended only when the param is set, so bucketless requests keep their pre-existing cache keys and warm entries survive this deploy.

Drop the serializer default (None) so drf-spectacular emits no default
for the param and generated clients cannot read it as nullable; omitted
now means the key is absent from validated_data, with the compute
function defaulting it. Append the cache-key bucket slot only when the
param is set, so pre-existing bucketless cache keys remain valid. Also
refresh the MCP tool-schema snapshot against the regenerated types.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
chatgpt-codex-connector[bot]

This comment was marked as outdated.

…lback cost folding

date_to is exclusive (timestamp < date_to), so a window whose end lands
exactly on a bucket boundary can never produce a row in that final
bucket; the cap check no longer counts it, letting users request the
full advertised 600-bucket window. Added a boundary test.

Also document on input_cost_usd that the uncached split only holds for
gateway-provided cost breakdowns: events priced by PostHog's ingestion
pipeline fold cache costs into input cost and leave the cache columns
at 0.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@github-actions
github-actions Bot requested a deployment to preview-pr-69891 July 15, 2026 07:25 In progress
chatgpt-codex-connector[bot]

This comment was marked as outdated.

The stored $ai_input_cost_usd includes prompt-cache read/write costs:
on events carrying the cache cost columns, input + output reconciles to
total and the cache costs sit inside input. Summing all four component
columns therefore double counted cache spend, inflating stacked charts.

by_bucket now derives uncached input per event as input minus cache
read/write, clamped at zero so a future switch to exclusive reporting
degrades to undercounting instead of double-subtracting. The four
components are now disjoint and sum to cost_usd when the breakdown is
present. The test fixture encodes the real inclusive semantics, so the
assertions fail without the derivation.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
chatgpt-codex-connector[bot]

This comment was marked as outdated.

… by_bucket

input_tokens claimed to be uncached, but whether cached tokens are
included in $ai_input_tokens follows the provider's reporting
($ai_cache_reporting_exclusive): Anthropic-style events exclude them,
OpenAI-style events include them. Describe the raw semantics and warn
against stacking with the cache token sums instead of normalizing on a
flag that is not reliably present.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@github-actions
github-actions Bot requested a deployment to preview-pr-69891 July 15, 2026 09:00 In progress

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cbd7c434f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Same cache as the US compute path, so repeat loads skip the cross-region hop.
cache_key = _cache_key(email, data["date_from"], data["date_to"], data["product"], data["limit"])
cache_key = _cache_key(
email, data["date_from"], data["date_to"], data["product"], data["limit"], data.get("bucket_minutes")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate bucket caps before EU cache hits

When the EU proxy serves a cached bucketed response, it returns before _compute_spend_analysis reruns the resolved-window cap check. With a relative end like date_to=-0hStart, the same raw cache key can be valid and cached just before an hour boundary, then resolve to more than 600 hourly buckets just after the boundary but still return the cached 200 for up to 5 minutes. Validate the resolved bucket count before this cache lookup, or key the cache by resolved bounds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted in 232ebd3: the resolve-and-validate window check is now a shared helper the EU proxy runs before its cache lookup, so a relative window that resolves over the cap 400s immediately instead of serving a stale cached 200 across the boundary. Side benefit: EU callers now get fast in-region 400s for all invalid windows instead of paying the cross-region hop. Test added asserting the over-cap request is rejected without an upstream call.

Comment thread products/ai_observability/frontend/generated/api.schemas.ts Outdated
…he cache lookup

The EU proxy caches on the raw date strings, so a relative window
cached while under the bucket cap could keep serving after it resolved
over the cap. The resolve-and-validate step is now a shared helper the
proxy runs before its cache lookup, which also gives EU callers fast
in-region 400s for invalid windows instead of paying the cross-region
hop.

Generated-By: PostHog Code
Task-Id: d7854448-4d18-44e7-b54a-d66e1ccddd0a
@webjunkie
webjunkie enabled auto-merge (squash) July 15, 2026 09:40
@webjunkie
webjunkie merged commit c2adacd into master Jul 15, 2026
257 checks passed
@webjunkie
webjunkie deleted the posthog-code/personal-spend-hourly-breakdown branch July 15, 2026 09:59
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-15 10:35 UTC Run
prod-us ✅ Deployed 2026-07-15 10:51 UTC Run
prod-eu ✅ Deployed 2026-07-15 10:50 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants